fix(auth): add startup-generated API key authentication to all routes#278
fix(auth): add startup-generated API key authentication to all routes#278advikdivekar wants to merge 2 commits into
Conversation
All API routes now require a valid key via Authorization: Bearer or X-Api-Key header. The key is generated at startup using secrets.token_hex and persisted to <data_dir>/.api_key (mode 0o600) so it survives restarts. Fixes utksh1#199 (zero API authentication — all endpoints publicly accessible). Also fixes is_filesystem_target() to match only real filesystem roots, preventing CIDR notation like 8.8.8.8/32 from bypassing safe-mode target validation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1456d51047
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| from sse_starlette.sse import EventSourceResponse | ||
|
|
||
| router = APIRouter(prefix="/api/v1") | ||
| router = APIRouter(prefix="/api/v1", dependencies=[Depends(require_api_key)]) |
There was a problem hiding this comment.
Exclude SSE stream route from global API-key dependency
Applying Depends(require_api_key) at router scope also protects /api/v1/task/{task_id}/stream, but the browser clients open that endpoint with native EventSource (frontend/src/api.ts and frontend/src/pages/TaskDetails.tsx), which cannot attach the required auth headers. As a result, scan progress streams will consistently fail with 401 in the UI after this change, even if non-stream API calls are later updated to send credentials.
Useful? React with 👍 / 👎.
| """Best-effort detection for path-based targets that should bypass host validation.""" | ||
| if target.startswith(("/", "./", "../", "~")): | ||
| # Absolute or relative filesystem roots only — not CIDR notation (e.g. 8.8.8.8/32) | ||
| if target.startswith(("/", "./", "../", "~/")): |
There was a problem hiding this comment.
Treat slash-containing relative paths as filesystem targets
The new is_filesystem_target() logic no longer recognizes relative paths like artifacts/memdump.raw (it only accepts /, ./, ../, ~/, or Windows-drive prefixes), so these values now fall into validate_target() and are rejected as invalid hostnames. This regresses non-code plugins that accept file/directory targets because previously slash-containing paths bypassed host validation; now users must rewrite existing inputs to add ./.
Useful? React with 👍 / 👎.
|
@utksh1 please review it and I have asked to claim few issues, please assign those issues to me under GSSoC 2026 |
What is the problem
Closes #199
All
/api/v1/*endpoints were publicly accessible with no authentication. Any process on localhost — or any attacker who can reach the port — could trigger scans, read results, and interact with the credential vault without supplying any credentials.What was changed
backend/secuscan/auth.py<data_dir>/.api_key(mode0600), exposesrequire_api_keyFastAPI dependencybackend/secuscan/main.pyinit_api_key(settings.data_dir)in the lifespan startup blockbackend/secuscan/routes.pydependencies=[Depends(require_api_key)]to theAPIRouter; fixesis_filesystem_target()CIDR false-positivetesting/backend/conftest.pytest_clientfixture now callsinit_api_keyand passes the key viaX-Api-Keyheadertesting/backend/integration/test_task_cleanup.pyapp_clientfixture initialises API key and passes it inAsyncClientheaderstesting/backend/test_task_pagination.pytest_clientfixture to avoid key state bleedtesting/backend/unit/test_api_auth.pyWhy this approach
.api_keysecrets.compare_digest— constant-time comparison prevents timing oracle attacks/api/v1/healthand/are defined directly onapp, not on the router, so they remain unauthenticated for health checksHow to test
Edge cases covered
0o600permissions on first run; reloaded on subsequent starts_api_key is None(service not yet initialised) returns 503, not 401Authorization: BearerandX-Api-Keyheaders accepted8.8.8.8/32) no longer incorrectly flagged as a filesystem targetVerification checklist
pytest testing/backend/— 261 passed (1 pre-existing failure intest_route_rejects_task_when_limiter_fulldue to rate-limiter state bleed across the full suite, also failing onmain)X-Api-Keyreturns 200Authorization: Bearerreturns 200is_filesystem_target("8.8.8.8/32")returnsFalse